from typing import Dict, Any, Literal
import warnings
import numpy as np
import pandas as pd


from ..dataclass.terminologies import GeoMaterialProps, GeoResults, Units
from ..dataclass.results import SingleResult, UnitResult
from ..dataclass.geoStructures import RecFoundation
from ..dataclass.geoProfiles import MultiProfile1D
from ..pipeline.pipeline import PipeModule, Port, status_manage
from ..pipeline.portTypes import PortTypeHint, PortType
from ..pipeline.pipeData import RangeModel, NestedRangeModel
from ..geocal.settleCal import cal_gama_m, RecFoundationSettle
from ..geocal.soilCapacityCal import SoilCapacityByExp, cal_modified_character_bearing_capacity
from ..dataclass.tables import FieldMetadata, TableData, TableCollection


@status_manage
class CreateRecFoundation(PipeModule):
    """Create a rectangular shallow foundation."""

    def __init__(self, mname: str = "CreateRecFoundation", auto_run: bool = True,
                 name: str | None = None, width: float = 8.0, length: float = 20.0, design_ground_elev: float = 0.0,
                 bottom_elev: float = -2.0, ave_unit_weight: float = 20, design_load: float = 2100, bottom_stress: float | None = None,
                 attributes_table: PortTypeHint.TableData | None = None,
                 attributes_table_map: dict[str, str] | None = None,
                 filter_name: str | None = None,
                 filter_value: int | float | str | None = None,
                 attributes_table_required: bool = False) -> None:
        """Initialize a CreateRecFoundation object.
        
        Parameters
        ----------
        name: str | None, default: None
            The name of the foundation.

        width (m): float, default: 8.0
            The width of the foundation.

        length (m): float, default: 20.0
            The length of the foundation.

        design_ground_elev (m): float, default: 0.0
            The design ground elevation.

        bottom_elev (m): float, default: -2.0
            The bottom elevation of the foundation.

        ave_unit_weight (kN/m³): float, default: 20
            The average unit weight of the foundation and its fill.

        design_load (kPa): float, default: 150
            The design load of the foundation.

        bottom_stress (kPa): float | None, default: None
            The bottom stress of the foundation.
            If it's not None, the design_load will be modified by the bottom_stress.

        attributes_table: PortTypeHint.TableData | PortTypeHint.TableCollection | None, default: None
            The attributes table data.
            If it's None, the module will try to get the value of attributes from this table.

        attributes_table_map: dict[str, str] | None, default: None
            The mapping of the attributes table's field names (titles) to the foundation's attributes names.
            Key is the attributes table's field name (title), value is the foundation's attribute name.

        filter_name: str | None, default: None
            The name or title of the field to filter the attributes table.

        filter_value: int | float | str | None, default: None
            The value of the field to filter the attributes table.

        attributes_table_required: bool, default: False
            Whether the attributes table is required.
            If it's True, the InputAttributesTable cannot be None, or None will be returned after execution.

        Note
        ----
        If the self.name is None, the module will not run even if the auto_run is True.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {"InputAttributesTable": Port(ptype=PortType.TableData, data=attributes_table,
                                                     pdoc="The attributes table data.")}
        self._ports_out = {"OutputFoundation": Port(ptype=PortType.RecFoundation, pdoc="The rectangular shallow foundation.")}

        self.name = name
        self.width = width
        self.length = length
        self.design_ground_elev = design_ground_elev
        self.bottom_elev = bottom_elev
        self.ave_unit_weight = ave_unit_weight
        self.design_load = design_load
        self.bottom_stress = bottom_stress

        self.attributes_table_map = attributes_table_map
        self.filter_name = filter_name
        self.filter_value = filter_value
        self.attributes_table_required = attributes_table_required

    @property
    def InputAttributesTable(self) -> None:
        raise AttributeError("Property 'InputAttributesTable' is write-only.")

    @InputAttributesTable.setter
    def InputAttributesTable(self, value: PortTypeHint.TableData | None) -> None:
        self["InputAttributesTable"] = value

    @property
    def OutputFoundation(self) -> PortTypeHint.RecFoundation | None:
        return self["OutputFoundation"]

    def set_cal_params(self, reset: bool = True) -> dict[str, RangeModel]:
        input_attributes: PortTypeHint.TableData | None = self._ports_in["InputAttributesTable"].data
        if input_attributes is None:
            filter_name_choices = None
            filter_value_choices = None
        else:
            filter_name_choices = input_attributes.field_titles
            if reset:
                self.filter_name = filter_name_choices[0]
            if self.filter_name is not None:
                filter_value_choices = input_attributes[self.filter_name].unique()
            else:
                filter_value_choices = None

        return {"name": RangeModel(vtype=("str", "None"), title="基础名称", default=self.name),
                "width": RangeModel(vtype="float", title="基础宽度b", units=Units.m, minmax=(0, None), inlcude_min=False, default=self.width),
                "length": RangeModel(vtype="float", title="基础长度l", units=Units.m, minmax=(0, None), inlcude_min=False, default=self.length),
                "design_ground_elev": RangeModel(vtype="float", title="设计地面标高", units=Units.m, default=self.design_ground_elev),
                "bottom_elev": RangeModel(vtype="float", title="基础底面标高", units=Units.m, default=self.bottom_elev),
                "ave_unit_weight": RangeModel(vtype="float", title="基础及回填土平均重度", units=Units.kN_m3, minmax=(5, 50), default=self.ave_unit_weight),
                "design_load": RangeModel(vtype="float", title="设计荷载", units=Units.kN, default=self.design_load, minmax=(0, None), inlcude_min=True),
                "bottom_stress": RangeModel(vtype="float", title="基础底面应力", units=Units.kPa, default=self.bottom_stress, minmax=(0, None), inlcude_min=True),
                "attributes_table_required": RangeModel(vtype="bool", title="是否需要属性表", default=self.attributes_table_required),
                "filter_name": RangeModel(vtype=("str", "None"), title="过滤字段", default=self.filter_name, choices=filter_name_choices),
                "filter_value": RangeModel(vtype=("int", "float", "str", "None"), title="过滤值", default=self.filter_value, 
                                           choices=filter_value_choices, depends_on=["filter_name"])}

    def execute(self) -> PortTypeHint.RecFoundation | None:
        attributes_table: PortTypeHint.TableData | None = self._ports_in["InputAttributesTable"].data
        if self.attributes_table_required:
            if attributes_table is None:
                self._ports_out["OutputFoundation"].data = None
                return None
            
        # Get attributes from the attributes table
        if self.attributes_table_map is not None:
            if self.filter_name is None or self.filter_value is None:
                attributes_values = attributes_table.iloc[0].to_dict()
            else:
                print(attributes_table[attributes_table[self.filter_name] == self.filter_value])
                attributes_values = attributes_table[attributes_table[self.filter_name] == self.filter_value].iloc[0].to_dict()
            for key, value in self.attributes_table_map.items():
                # Key is the attributes table's field name (title), value is the foundation's attribute name.
                # Check whether the key is a field title or name
                if key in attributes_table.field_titles:
                    key = attributes_table.title_to_name[key]
                if value not in self.__dict__:
                    raise ValueError(f"Attribute '{value}' not found in the module's attribute.")
                setattr(self, value, attributes_values[key])

        if self.name is None:
            self._ports_out["OutputFoundation"].data = None
            return None
        
        foundation = RecFoundation(name=self.name, width=self.width, length=self.length, design_ground_elev=self.design_ground_elev,
                               bottom_elev=self.bottom_elev, ave_unit_weight=self.ave_unit_weight, design_load=self.design_load)
        if self.bottom_stress:  # Modify the design_load by the bottom_stress
            foundation.bottom_stress = self.bottom_stress

        self._ports_out["OutputFoundation"].data = foundation
        return foundation


@status_manage
class FoundationSettlement(PipeModule):
    """Calculate the settlement of a shallow foundation."""

    def __init__(self, mname: str = "FoundationSettlement", auto_run: bool = True,
                 profiles: PortTypeHint.MultiProfile1D | None = None,
                 material_table: PortTypeHint.MaterialTable | None = None,
                 foundation: PortTypeHint.RecFoundation | None = None,
                 cal_p0_from: Literal["design_ground", "original_ground"] = "original_ground",
                 fill_compression_modulus: float = 2,
                 fill_characteristic_bearing_capacity: float = 80,
                 fill_unit_weight: float = 18,
                 compression_modulus_by_profile: dict[str, list[float]] | None = None,
                 design_water_elev: float | None = None,
                 water_unit_weight: float = 10) -> None:
        """Initialize a FoundationSettlement object.

        Parameters
        ----------
        profiles : PortTypeHint.MultiProfile1D, default: None
            The 1D geological profiles.

        material_table : PortTypeHint.MaterialTable, default: None
            The material table for the geological profiles.
            Example:
            ```
            materials_table = {MaterialProp.MaterialID: [0, 1, 2],
                              MaterialProp.CompressionModulus: [3.5, 3.1, 6.5],
                              MaterialProp.SoilCharacteristicBearingCapacity: [100, 100, 100],
                              MaterialProp.UnitWeight: [18, 17, 18],
                              MaterialProp.SaturateUnitWeight: [19, 20, 19]}
            units = [null, MPa, kPa, kN/m3, kN/m3]
            ```

        foundation : PortTypeHint.RecFoundation, default: None
            The foundation used to calculate the settlement.

        cal_p0_from : Literal["design_ground", "original_ground"] = "original_ground", default: None
            The reference level used to calculate the additional stess p0.

        fill_compression_modulus (MPa): float, default: 2
            The compression modulus of the fill.

        fill_characteristic_bearing_capacity (kPa): float, default: 80
            The characteristic bearing capacity of the fill.

        fill_unit_weight (kN/m3): float, default: 18
            The unit weight of the fill.

        compression_modulus_by_profile : dict[str, list[float]], default: None
            The compression modulus of the geological profiles.
            Key is the profile name, value is the compression modulus of the profile for each layer (MPa).
            If None, the compression modules from the material table will be used.
            If the compression modulus for a profile is not None, it will be used instead of the compression modulus from the material table.
            This usually used when considering the change of compression modulus due to the change of stress.
            Example:
            ```
            compression_modulus_by_profile = {"ZK1": [3.5, 3.1, 6.5], "ZK2": [2.5, 2.1, 5.5]}
            ```
            The length of the values in the list should be the same as the number of layers in the profile.

        design_water_elev (m): float, default: None
            The design water elevation.
            If it's None, the water depth in each profile will be used.

        water_unit_weight (kN/m3): float, default: 10
            The unit weight of water.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {"InputProfiles": Port(ptype=PortType.MultiProfile1D, data=profiles,
                                                pdoc="The 1D profiles used for calculation."),
                          "InputMaterialTable": Port(ptype=PortType.MaterialTable, data=material_table,
                                                 pdoc="The material table used for calculation."),
                          "InputFoundation": Port(ptype=PortType.RecFoundation, data=foundation,
                                                 pdoc="The information of the shallow foundation.")}
        self._ports_out = {"OutputResult": Port(ptype=PortType.TableData, pdoc="The calculated result.")}

        self.cal_p0_from = cal_p0_from
        self.fill_compression_modulus = fill_compression_modulus
        self.fill_characteristic_bearing_capacity = fill_characteristic_bearing_capacity
        self.fill_unit_weight = fill_unit_weight
        self.compression_modulus_by_profile = compression_modulus_by_profile
        self.design_water_elev = design_water_elev
        self.water_unit_weight = water_unit_weight

    @property
    def InputProfiles(self) -> None:
        raise AttributeError("Property 'InputProfiles' is write-only.")

    @InputProfiles.setter
    def InputProfiles(self, value: PortTypeHint.Profile1D | None) -> None:
        self["InputProfiles"] = value

    @property
    def InputMaterialTable(self) -> None:
        raise AttributeError("Property 'InputMaterialTable' is write-only.")

    @InputMaterialTable.setter
    def InputMaterialTable(self, value: PortTypeHint.MaterialTable | None) -> None:
        self["InputMaterialTable"] = value

    @property
    def InputFoundation(self) -> None:
        raise AttributeError("Property 'InputFoundation' is write-only.")

    @InputFoundation.setter
    def InputFoundation(self, value: PortTypeHint.RecFoundation | None) -> None:
        self["InputFoundation"] = value

    @property
    def OutputResult(self) -> PortTypeHint.TableData | None:
        return self["OutputResult"]

    def set_cal_params(self, reset: bool = True) -> dict[str, RangeModel]:
        profiles: PortTypeHint.MultiProfile1D | None = self._ports_in["InputProfiles"].data
        if profiles is None:
            compression_modulus_by_profile_key_choices = None
        else:
            compression_modulus_by_profile_key_choices = [profile.pnum for profile in profiles]

        if self.cal_p0_from == "original_ground":
            fill_required = False
        else:
            fill_required = True

        values_range = {"compression_modulus_by_profile": RangeModel(vtype=("dict", "None"), title="按层输入压缩模量", default=self.compression_modulus_by_profile,
                                                                     dict_value_type="list", list_type="float",
                                                                     dict_key_choices=compression_modulus_by_profile_key_choices, required=False),
                        "design_water_elev": RangeModel(vtype="float", title="设计水位标高", default=self.design_water_elev, required=False),
                        "cal_p0_from": RangeModel(vtype="str", title="基底以上土体自重应力计算方式", default=self.cal_p0_from,
                                                  choices=["original_ground", "design_ground"], choices_label=["从天然地面计算", "从设计地面计算"]),
                        "fill_compression_modulus": RangeModel(vtype="float", title="填土压缩模量", default=self.fill_compression_modulus, 
                                                               depends_on=["cal_p0_from"], required=fill_required),
                        "fill_unit_weight": RangeModel(vtype="float", title="填土重度", default=self.fill_unit_weight, 
                                                      depends_on=["cal_p0_from"], required=fill_required),
                        "fill_characteristic_bearing_capacity": RangeModel(vtype="float", title="填土地基承载力特征值", 
                                                                           default=self.fill_characteristic_bearing_capacity, 
                                                                           depends_on=["cal_p0_from"], required=fill_required)}
        return values_range

    def execute(self) -> PortTypeHint.TableData | None:
        profiles: PortTypeHint.MultiProfile1D | None = self._ports_in["InputProfiles"].data
        materials: PortTypeHint.MaterialTable | None = self._ports_in["InputMaterialTable"].data
        foundation: PortTypeHint.RecFoundation | None = self._ports_in["InputFoundation"].data

        if profiles is None or materials is None or foundation is None:
            self._ports_out["OutputResult"].data = None
            return None
        
        # Add fill properties to material table
        if -1 not in materials.index:    # No fill properties in material table
            materials.add_material({GeoMaterialProps.MaterialID: -1, GeoMaterialProps.MaterialName: "填土", 
                                    GeoMaterialProps.LayerNumber: "0", 
                                    GeoMaterialProps.CompressionModulus: self.fill_compression_modulus,
                                    GeoMaterialProps.UnitWeight: self.fill_unit_weight,
                                    GeoMaterialProps.SaturateUnitWeight: self.fill_unit_weight,
                                    GeoMaterialProps.SoilCharacteristicBearingCapacity: self.fill_characteristic_bearing_capacity})
            
        # Modify the design water elevation
        if self.design_water_elev is not None:
             for profile in profiles:
                new_gwt_depth = profile.top - self.design_water_elev
                # Update the profiles_table
                profiles.profiles_table.loc[profiles.profiles_table['pnum'] == profile.pnum, 'gwt_depth'] = new_gwt_depth

        ##################
        # Calculate p0 - additional stress kPa
        ##################
        stress_from_load = foundation.bottom_stress
        p0s = {}    # store p0 for each profile, key is the pnum, value is the value of p0
        if self.cal_p0_from == "design_ground":
            for profile in profiles:
                # If profile top < design ground, add fill - the material id of fill is -1.
                if profile.top < foundation.design_ground_elev:
                    profile = profile.cut_profile(cut_elev=foundation.design_ground_elev, add_fill=True)
                # Find the profile within the foundation
                profile_within_f = profile.slice_profile(slice_top=foundation.design_ground_elev,
                                                         slice_bottom=foundation.bottom_elev)
                # Calculate gama_m and delta_sigma_z
                gama_m = cal_gama_m(profile_within_f, material_table=materials, water_unit_wight=self.water_unit_weight)
                dela_sigma_z = (foundation.ave_unit_weight - gama_m) * foundation.bottom_depth
                # Calculate p0
                p0 = stress_from_load + dela_sigma_z
                p0s[profile.pnum] = p0
        elif self.cal_p0_from == "original_ground":
            for profile in profiles:
                if profile.top <= foundation.bottom_elev:   # profile top < foundation bottom
                    if profile.gwt_depth is None or profile.gwt_elevation <= foundation.bottom_elev:   # no water
                        delta_sigma_z = foundation.ave_unit_weight * foundation.bottom_depth
                    else:
                        delta_sigma_z = (foundation.ave_unit_weight * foundation.bottom_depth -
                                         self.water_unit_weight * (profile.gwt_elevation - foundation.bottom_elev))
                else:
                    # Find the profile above the foundation bottom
                    profile_above_f = profile.cut_profile(cut_elev=foundation.bottom_elev, keep_below=False)
                    # Calculate gama_m and delta_sigma_z
                    gama_m = cal_gama_m(profile_above_f, material_table=materials, water_unit_wight=self.water_unit_weight)
                    if profile_above_f.gwt_depth is None or profile_above_f.gwt_depth >= 0:
                        delta_sigma_z = (foundation.ave_unit_weight * foundation.bottom_depth -
                                         gama_m * profile_above_f.layers_depth[-1])
                    else:
                        delta_sigma_z = (foundation.ave_unit_weight * foundation.bottom_depth -
                                         self.water_unit_weight * (profile_above_f.gwt_elevation - profile_above_f.top)
                                         - gama_m * profile_above_f.layers_depth[-1])
                    # Calculate p0
                    p0 = stress_from_load + delta_sigma_z
                    p0s[profile.pnum] = p0
        else:
            raise ValueError(f"Attribute 'cal_p0_from' cannot be {self.cal_p0_from}!")

        ##################
        # Calculate the settlement
        ##################
        # Get delta_z which is used to decide the depth of settlement calculation according to GB 50007-2011
        if foundation.width <= 2:
            delta_z = 0.3
        elif foundation.width <= 4:
            delta_z = 0.6
        elif foundation.width <= 8:
            delta_z = 0.8
        else:
            delta_z = 1.0

        # Calculate the settlement by each profile
        final_result = {GeoResults.ProfileNumber: [], "cal_settle_depth": [], "bore_depth": [], "is_depth_enough": [],
                        "addition_stress": [], "settlement": [], "empirical_psi": [], "modified_settlement": [],
                        "infer_settlement": [], "infer_empirical_psi": [], "infer_modified_settlement": []}
        settle_cal = RecFoundationSettle(width=foundation.width, length=foundation.length,
                                         ground_elev=foundation.design_ground_elev,
                                         bottom_depth=foundation.bottom_depth)
        for profile in profiles:
            # get the profile below the bottom of the foundation
            cal_profile = profile.cut_profile(cut_elev=foundation.bottom_elev, add_fill=True)
            # get the depth of each interface in the profile
            interfaces_depth = cal_profile.layers_depth.copy()
            interfaces_depth.insert(0, 0)   # Insert the depth of the 1st layer's top
            # Get the compression modules of each layer in the profile
            materials_id = cal_profile.materials_id
            layers_es = []
            if self.compression_modulus_by_profile and (profile.pnum in self.compression_modulus_by_profile):
                number_of_layers = len(profile.layers_depth)
                number_of_values = len(self.compression_modulus_by_profile[profile.pnum])
                if number_of_values != number_of_layers:
                    raise ValueError(f"The length of the compression modulus values in the list should be the same as the number of layers in the profile for '{profile.pnum}'!")
                # How many layers above the foundation bottom
                number_of_layers_above_f = number_of_layers - len(cal_profile.layers_depth)
                layers_es = self.compression_modulus_by_profile[profile.pnum][number_of_layers_above_f:]
            else:
                for material_id in materials_id:
                    layer_es = materials.get_material(material_id)[GeoMaterialProps.CompressionModulus]
                    layers_es.append(layer_es)
            # calculate the settlement
            settle_results = settle_cal.total_settle(profile=interfaces_depth, es=layers_es, p0=p0s[profile.pnum],
                                                     delta_z=delta_z)

            # modify the settlement by empirical coefficient
            # use the fak of the layer at the bottom of the foundation to modify the settlement
            fak = materials.get_material(materials_id[0])[GeoMaterialProps.SoilCharacteristicBearingCapacity]
            if np.isnan(fak) or fak is None:
                raise ValueError(f"钻孔编号为 '{profile.pnum}' 的地基土承载力特征值为空，请检查岩土参数!")

            # profile depth > settle calculation depth
            is_depth_enough = settle_results["is_depth_enough"]
            f_settle = settle_results["settlement"]
            settle_cal_depth = settle_results["settle_cal_depth"]
            infer_settle = settle_results["infer_settlement"]
            if is_depth_enough:
                bottom_layer_id = settle_results["bottom_layer_id"]     # the layer id of the first layer is 0
                if bottom_layer_id < len(interfaces_depth) - 2:
                    del interfaces_depth[bottom_layer_id+2:]
                interfaces_depth[-1] = settle_cal_depth
                empirical_psi = settle_cal.settle_empirical_psi(interfaces_depth, layers_es, fak, p0s[profile.pnum])
                infer_empirical_psi = empirical_psi
            else:
                empirical_psi = settle_cal.settle_empirical_psi(interfaces_depth, layers_es, fak, p0s[profile.pnum])
                interfaces_depth[-1] = settle_cal_depth
                infer_empirical_psi = settle_cal.settle_empirical_psi(interfaces_depth, layers_es, fak, p0s[profile.pnum])

            final_result[GeoResults.ProfileNumber].append(profile.pnum)
            final_result["cal_settle_depth"].append(settle_cal_depth)
            final_result["bore_depth"].append(profile.layers_depth[-1])
            if is_depth_enough:
                final_result["is_depth_enough"].append("满足")
            else:
                final_result["is_depth_enough"].append("不满足")

            final_result["addition_stress"].append(p0s[profile.pnum])
            final_result["settlement"].append(f_settle)
            final_result["empirical_psi"].append(empirical_psi)
            final_result["modified_settlement"].append(empirical_psi * f_settle)

            final_result["infer_settlement"].append(infer_settle)
            final_result["infer_empirical_psi"].append(infer_empirical_psi)
            final_result["infer_modified_settlement"].append(infer_empirical_psi * infer_settle)
            
        final_result = pd.DataFrame(final_result)
        # Set FieldMetadata
        fields_metadata = [
            FieldMetadata(name=GeoResults.ProfileNumber, title="钻孔编号", unit=Units.UNITLESS),
            FieldMetadata(name="cal_settle_depth", title="变形计算深度", unit=Units.m),
            FieldMetadata(name="bore_depth", title="钻孔深度", unit=Units.m),
            FieldMetadata(name="is_depth_enough", title="钻孔深度是否满足", unit=Units.UNITLESS),
            FieldMetadata(name="addition_stress", title="附加应力", unit=Units.kPa),
            FieldMetadata(name="settlement", title="基础沉降", unit=Units.mm),
            FieldMetadata(name="empirical_psi", title="沉降经验系数", unit=Units.UNITLESS),
            FieldMetadata(name="modified_settlement", title="修正基础沉降", unit=Units.mm),
            FieldMetadata(name="infer_settlement", title="推测基础沉降", unit=Units.mm),
            FieldMetadata(name="infer_empirical_psi", title="推测沉降经验系数", unit=Units.UNITLESS),
            FieldMetadata(name="infer_modified_settlement", title="推测修正基础沉降", unit=Units.mm),
        ]

        output = TableData(final_result, name="f_settle", title="基础沉降计算结果", fields_meta=fields_metadata)
        self._ports_out["OutputResult"].data = output
        return output


@status_manage
class BearingCapacityModify(PipeModule):
    """Modify the bearing capacity of soil according to shape of the foundation."""

    def __init__(self, mname: str = "BearingCapacityModify", auto_run: bool = True,
                 profiles: PortTypeHint.MultiProfile1D | None = None,
                 materials: PortTypeHint.MaterialTable | None = None,
                 foundation: PortTypeHint.RecFoundation | None = None,
                 cal_d_from: Literal["original_ground", "design_ground", "user_defined"] = "design_ground",
                 equivalent_f_depth: float | None = None,
                 f_soil_types: dict[str, int] | None = None,
                 design_water_elev: float | None = None, 
                 water_unit_weight: float = 10, fill_character_bearing_capacity: float = 100,
                 fill_unit_weight: float = 18) -> None:
        """Initialize BearingCapacityModify object.

        Parameters
        ----------
        profiles: PortTypeHint.Profile1D | PortTypeHint.MultiProfile1D
            The 1D profiles used for the calculation.

        materials : PortTypeHint.MaterialTable, default: None
            The material table for the geological profiles.

        foundation : PortTypeHint.RecFoundation, default: None
            The information of the shallow foundation.

        cal_d_from: Literal["design_ground", "original_ground", "user_defined"], default: "design_ground"
            From where to calculate the bottom depth of the foundation.
            If "user_defined", the calcuation of gama_m will use "design_ground" as the reference level.
                But the "d" in the final formula (f_ak + η_b * γ * (b-3) + η_d * γ_m * (d-0.5) ) will use the equivalent_f_depth.

        equivalent_f_depth (m): float, default: None
            The equivalent depth of the foundation.
            Only used when cal_d_from is "user_defined".
            对于两侧裙楼分别都大于两倍主楼宽度，则需两侧分别折算等效基础埋深，取不利。

        f_soil_types: dict[str, int] | None, default: None
            The soil types of soil right below foundation to decide eta_b and eta_d. Key should be the pnum of the
            profile, value is the foundation soil type id. Below is the table for the soil type id:
            ```
            | 土的类别                           | ID  | eta_b  | eta_d  |
            |----------------------------------|------|------|------|
            | 淤泥和淤泥质土                     | 0    | 0    | 1    |
            | 人工填土，e或t大于等于0.85的黏性土 | 1    | 0    | 1    |
            | 红黏土，含水比 aw > 0.8           | 2    | 0    | 1.2  |
            | 红黏土，含水比 aw ≤ 0.8           | 3    | 0.15 | 1.4  |
            | 大面积压实填土，压实系数大于0.95，黏粒含量pc ≥ 10%的黏性土 | 4 | 0 | 1.5 |
            | 最大干密度大于2100kg/m³的级配砂石  | 5    | 0    | 2    |
            | 粉土，黏粒含量 pc ≥ 10%的粉性土    | 6    | 0.3  | 1.5  |
            | 粉土，黏粒含量 pc < 10%的粉性土     | 7    | 0.5  | 2    |
            | e及t均小于0.85的黏性土             | 8    | 0.3  | 1.6  |
            | 粉砂、细砂（不包括饱湿与饱和时的高密实状态） | 9 | 2 | 3 |
            | 中砂、粗砂、砾砂和碎石土           | 10   | 3    | 4.4  |
            | 非弱风化和全风化岩石               | 11   | 1    | 1    |
            ```
            For loess, here is the soil type table:
            ```
            | 土的类别                           | ID  | eta_b  | eta_d  |
            |----------------------------------|------|------|------|
            | 晚更新世、全新世湿陷性黄土，ω≤24%   | 12   | 0.2  | 1.25 |
            | 晚更新世、全新世湿陷性黄土，ω>24%   | 13   | 0    | 1.1 |
            | 新进堆积黄土                       | 14   | 0   | 1.0  |
            | 饱和黄土，e及IL都小于0.85          |  15  | 0.2  | 1.25 |
            | 饱和黄土，e或IL大于等于0.85        | 16   | 0    | 1.1 |
            | 饱和黄土，e及IL都不小于1.00        | 17   | 0    | 1.0 |
            ```
            If the foundation soil type is not specified for a profile, this profile will be skipped.

        design_water_elev (m): float, default: None
            The design ground water elevation.
            If None, there is no groundwater.

        water_unit_weight (kN/m³): float, default: 10
            The unit weight of the ground water.

        fill_character_bearing_capacity (kPa): float, default: 100
            The characteristic bearing capacity of the fill.

        fill_unit_weight (kN/m³): float, default: 18
            The unit weight of the fill.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {"InputProfiles": Port(ptype=PortType.MultiProfile1D, data=profiles,
                                                pdoc="The 1D profiles used for calculation."),
                          "InputMaterialTable": Port(ptype=PortType.MaterialTable, data=materials,
                                                 pdoc="The material table used for calculation."),
                          "InputFoundation": Port(ptype=PortType.RecFoundation, data=foundation,
                                                 pdoc="The information of the shallow foundation.")}
        self._ports_out = {"OutputResult": Port(ptype=PortType.TableCollection, pdoc="The calculated result.")}

        self.cal_d_from = cal_d_from
        self.equivalent_f_depth = equivalent_f_depth
        self.f_soil_types = f_soil_types
        self.design_water_elev = design_water_elev
        self.water_unit_weight = water_unit_weight
        self.fill_character_bearing_capacity = fill_character_bearing_capacity
        self.fill_unit_weight = fill_unit_weight

    @property
    def InputProfiles(self) -> None:
        raise AttributeError("Property 'InputProfiles' is write-only.")

    @InputProfiles.setter
    def InputProfiles(self, value: PortTypeHint.Profile1D | None) -> None:
        self["InputProfiles"] = value

    @property
    def InputMaterialTable(self) -> None:
        raise AttributeError("Property 'InputMaterialTable' is write-only.")

    @InputMaterialTable.setter
    def InputMaterialTable(self, value: PortTypeHint.MaterialTable | None) -> None:
        self["InputMaterialTable"] = value

    @property
    def InputFoundation(self) -> None:
        raise AttributeError("Property 'InputFoundation' is write-only.")

    @InputFoundation.setter
    def InputFoundation(self, value: PortTypeHint.RecFoundation | None) -> None:
        self["InputFoundation"] = value

    @property
    def OutputResult(self) -> PortTypeHint.TableCollection | None:
        return self["OutputResult"]

    def set_cal_params(self, reset: bool = True) -> dict[str, RangeModel]:
        profiles: PortTypeHint.MultiProfile1D | None = self._ports_in["InputProfiles"].data
        foundation: PortTypeHint.RecFoundation | None = self._ports_in["InputFoundation"].data

        if foundation is not None:
            if self.equivalent_f_depth is None or reset:
                self.equivalent_f_depth = foundation.bottom_depth

        if profiles is None:
            bores_numbers = None
        else:
            bores_numbers = [profile.pnum for profile in profiles]

        equivalent_f_depth_visible = False
        if self.cal_d_from == "user_defined":
            equivalent_f_depth_visible = True

        values_range = {"cal_d_from": RangeModel(vtype="str", title="基础埋深计算方式", choices=("original_ground", "design_ground", "user_defined"),
                                                 choices_label=("从天然地面计算", "从设计地面计算", "自定义")),
                        "equivalent_f_depth": RangeModel(vtype="float", title="等效基础埋深", default=self.equivalent_f_depth, units=Units.m, minmax=(0, None),
                                                         depends_on="cal_d_from", visible=equivalent_f_depth_visible),
                        "f_soil_types": RangeModel(vtype="dict", title="基础底面所在土的类别", default=self.f_soil_types, dict_value_type="int",
                                                   dict_key_choices=bores_numbers,
                                                   dict_value_choices=tuple(range(18)),
                                                   dict_key_choices_label=("淤泥和淤泥质土", 
                                                                           "人工填土，e或t大于等于0.85的黏性土", 
                                                                           "红黏土，含水比 aw > 0.8", 
                                                                           "红黏土，含水比 aw ≤ 0.8", 
                                                                           "大面积压实填土，压实系数大于0.95，黏粒含量pc ≥ 10%的黏性土", 
                                                                           "最大干密度大于2100kg/m³的级配砂石", 
                                                                           "粉土，黏粒含量 pc ≥ 10%的粉性土", 
                                                                           "粉土，黏粒含量 pc < 10%的粉性土", "e及t均小于0.85的黏性土", 
                                                                           "粉砂、细砂（不包括饱湿与饱和时的高密实状态）", "中砂、粗砂、砾砂和碎石土", 
                                                                           "非弱风化和全风化岩石", "晚更新世、全新世湿陷性黄土，ω≤24%", 
                                                                           "晚更新世、全新世湿陷性黄土，ω>24%", "新进堆积黄土", 
                                                                           "饱和黄土，e及IL都小于0.85", "饱和黄土，e或IL大于等于0.85", 
                                                                           "饱和黄土，e及IL都不小于1.00")),
                        "design_water_elev": RangeModel(vtype="float", title="水位高程", default=self.design_water_elev, units=Units.m, required=False),
                        "fill_character_bearing_capacity": RangeModel(vtype="float", title="填土地基承载力特征值", default=self.fill_character_bearing_capacity,
                                                                      units=Units.kPa, minmax=(0, None), inlcude_min=False),
                        "fill_unit_weight": RangeModel(vtype="float", title="填土重度", default=self.fill_unit_weight,
                                                        units=Units.kN_m3, minmax=(0, None), inlcude_min=False)}
        return values_range

    def execute(self) -> PortTypeHint.TableCollection | None:
        profiles: PortTypeHint.MultiProfile1D | None = self._ports_in["InputProfiles"].data
        materials: PortTypeHint.MaterialTable | None = self._ports_in["InputMaterialTable"].data
        foundation: PortTypeHint.RecFoundation | None = self._ports_in["InputFoundation"].data

        if profiles is None or materials is None or foundation is None:
            self._ports_out["OutputResult"].data = None
            return None

        if not self.f_soil_types:
            self.f_soil_types = {pnum: 0 for pnum in [profile.pnum for profile in profiles]}

        # Modify the ground water depth in each profile by design ground water elevation
        if self.design_water_elev:
            for profile in profiles:
                new_gwt_depth = profile.top - self.design_water_elev
                # Update the profiles_table
                profiles.profiles_table.loc[profiles.profiles_table['pnum'] == profile.pnum, 'gwt_depth'] = new_gwt_depth
        else:
            for profile in profiles:
                # Update the profiles_table
                profiles.profiles_table.loc[profiles.profiles_table['pnum'] == profile.pnum, 'gwt_depth'] = None

         # Add fill characteristic bearing capacity and unit weight to material table
        if -1 not in materials.index:    # No fill properties in material table
            materials.add_material({GeoMaterialProps.MaterialID: -1, GeoMaterialProps.MaterialName: "填土",
                                   GeoMaterialProps.LayerNumber: "0",
                                   GeoMaterialProps.SoilCharacteristicBearingCapacity: self.fill_character_bearing_capacity,
                                   GeoMaterialProps.UnitWeight: self.fill_unit_weight})

        ##############################################################################
        # Calculate gama_m - average unit weight of soil above foundation bottom kN/m3
        ##############################################################################
        gama_ms = {}  # store gama for each profile, key is the pnum, value is the value of gama
        ds = {}     # store the foundation bottom depth, key is the pnum, value is the value of d.
        if self.cal_d_from == "design_ground" or self.cal_d_from == "user_defined":
            for profile in profiles:
                d = foundation.bottom_depth
                # If profile top < design ground, add fill - the material id of fill is -1.
                if profile.top < foundation.design_ground_elev:
                    profile = profile.cut_profile(cut_elev=foundation.design_ground_elev, add_fill=True, fill_id=-1)
                # Find the profile within the foundation
                profile_within_f = profile.slice_profile(slice_top=foundation.design_ground_elev,
                                                         slice_bottom=foundation.bottom_elev)
                if profile_within_f.top is None:
                    raise RuntimeError(f"基础底面至设计地面标高范围和钻孔 {profile.pnum} 不相交，无法计算！")
                # Calculate gama_m
                gama_m = cal_gama_m(profile_within_f, material_table=materials, use_effective_weight=True,
                                    water_unit_wight=self.water_unit_weight)

                gama_ms[profile.pnum] = gama_m
                ds[profile.pnum] = d

        elif self.cal_d_from == "original_ground":
            for profile in profiles:
                if profile.top <= foundation.bottom_elev:   # profile is below the bottom of foundation
                    d = 0
                    gama_m = 0
                else:
                    d = profile.top - foundation.bottom_elev
                    # Find the profile above the foundation bottom
                    profile_above_f = profile.cut_profile(cut_elev=foundation.bottom_elev, keep_below=False)
                    # Calculate gama_m
                    gama_m = cal_gama_m(profile_above_f, material_table=materials, use_effective_weight=True,
                                        water_unit_wight=self.water_unit_weight)

                gama_ms[profile.pnum] = gama_m
                ds[profile.pnum] = d

        else:
            raise ValueError(f"Attribute 'cal_d_from' cannot be {self.cal_d_from}!")

        ###############################################################
        # Calculate the modified soil characteristic bearing capacity
        ###############################################################
        result_by_profile = {"foundation_name": [],
                             GeoResults.ProfileNumber: [],
                             GeoMaterialProps.LayerNumber: [], 
                             GeoMaterialProps.SoilCharacteristicBearingCapacity: [],
                             "unit_weight_below_foundation": [],
                             "unit_weight_above_foundation": [],
                             GeoMaterialProps.SoilBearingCapacityFactorEtaB: [],
                             GeoMaterialProps.SoilBearingCapacityFactorEtaD: [],
                             GeoResults.FoundationDepth: [],
                             GeoMaterialProps.ModifiedSoilCharacteristicBearingCapacity: []}
        satisfied = "满足"
        for profile in profiles:
            f_soil_type = self.f_soil_types.get(profile.pnum)
            if f_soil_type is None:   # If there is no corresponding foundation soil type
                warnings.warn(f"No foundation soil type is specified to profile '{profile.pnum}'!", UserWarning)
                continue
            # get the profile below the bottom of the foundation
            cal_profile = profile.cut_profile(cut_elev=foundation.bottom_elev, add_fill=True, fill_id=-1)
            if cal_profile.top is None:
                raise RuntimeError(f"钻孔 {profile.pnum} 底部位于基础底面以上，无法计算！")
            # Get fak of foundation soil
            f_soil_props = materials.get_material(cal_profile.materials_id[0])  # material properties of foundation soil
            fak = f_soil_props[GeoMaterialProps.SoilCharacteristicBearingCapacity]
            # Get the layer number of the foundation soil
            f_layer_number = f_soil_props[GeoMaterialProps.LayerNumber]
            # Check if DeepPlateLoadingTest column exists in the soil properties
            from_DPLT = False
            if GeoMaterialProps.DeepPlateLoadingTest in f_soil_props:
                from_DPLT = f_soil_props[GeoMaterialProps.DeepPlateLoadingTest]
            # Get gama and gama_m
            gama_m = gama_ms[profile.pnum]
            if cal_profile.gwt_depth is None:
                gama = f_soil_props[GeoMaterialProps.UnitWeight]
            else:
                if cal_profile.gwt_elevation > cal_profile.layers_elevation[0]:
                    gama = f_soil_props[GeoMaterialProps.SaturateUnitWeight] - self.water_unit_weight
                else:
                    gama = f_soil_props[GeoMaterialProps.UnitWeight]
            # Get d - foundation bottom depth
            d = ds[profile.pnum]
            # Calculate fa, eta_b and eta_d
            if self.cal_d_from == "user_defined":
                result = cal_modified_character_bearing_capacity(fak=fak, soil_type=f_soil_type, gama=gama, gama_m=gama_m,
                                                                 f_width=foundation.width, f_depth=self.equivalent_f_depth, from_DPLT=from_DPLT)
            else:
                result = cal_modified_character_bearing_capacity(fak=fak, soil_type=f_soil_type, gama=gama, gama_m=gama_m,
                                                                 f_width=foundation.width, f_depth=d, from_DPLT=from_DPLT)
            # Assign result
            result_by_profile["foundation_name"].append(foundation.name)
            result_by_profile[GeoResults.ProfileNumber].append(profile.pnum)
            result_by_profile[GeoMaterialProps.LayerNumber].append(f_layer_number)
            result_by_profile[GeoMaterialProps.SoilCharacteristicBearingCapacity].append(fak)
            result_by_profile["unit_weight_below_foundation"].append(gama)
            result_by_profile["unit_weight_above_foundation"].append(gama_m)
            result_by_profile[GeoMaterialProps.SoilBearingCapacityFactorEtaB].append(result["eta_b"])
            result_by_profile[GeoMaterialProps.SoilBearingCapacityFactorEtaD].append(result["eta_d"])
            if self.cal_d_from == "user_defined":
                result_by_profile[GeoResults.FoundationDepth].append(self.equivalent_f_depth)
            else:
                result_by_profile[GeoResults.FoundationDepth].append(d)
            result_by_profile[GeoMaterialProps.ModifiedSoilCharacteristicBearingCapacity].append(result["fa"])
            
            # Get conclusion
            if result["fa"] < foundation.bottom_stress:
                satisfied = "不满足"

        result_by_foundation = {"foundation_name": [foundation.name],
                                "foundation_width": [foundation.width],
                                "bottom_stress": [foundation.bottom_stress],
                                "satisfied": [satisfied]}

        # Assign the result to TableCollection
        # Create the fields metadata of result_by_profile
        fields_metadata = [FieldMetadata(name="foundation_name", title="拟建物", unit=Units.UNITLESS),
                           FieldMetadata(name=GeoResults.ProfileNumber, title="钻孔编号", unit=Units.UNITLESS),
                           FieldMetadata(name=GeoMaterialProps.LayerNumber, title="基础持力层", unit=Units.UNITLESS),
                           FieldMetadata(name=GeoMaterialProps.SoilCharacteristicBearingCapacity, title=GeoMaterialProps.SoilCharacteristicBearingCapacity.title, unit=Units.kPa),
                           FieldMetadata(name="unit_weight_below_foundation", title="基底以下土重\gamma", unit=Units.kN_m3),
                           FieldMetadata(name="unit_weight_above_foundation", title="基底以上土重\gamma_m", unit=Units.kN_m3),
                           FieldMetadata(name=GeoMaterialProps.SoilBearingCapacityFactorEtaB, title=GeoMaterialProps.SoilBearingCapacityFactorEtaB.title, unit=Units.UNITLESS),
                           FieldMetadata(name=GeoMaterialProps.SoilBearingCapacityFactorEtaD, title=GeoMaterialProps.SoilBearingCapacityFactorEtaD.title, unit=Units.UNITLESS),
                           FieldMetadata(name=GeoResults.FoundationDepth, title=GeoResults.FoundationDepth.title, unit=Units.m),
                           FieldMetadata(name=GeoMaterialProps.ModifiedSoilCharacteristicBearingCapacity, title=GeoMaterialProps.ModifiedSoilCharacteristicBearingCapacity.title, unit=Units.kPa),
                           ]
        table_result_by_profile = TableData(data=result_by_profile, 
                                            name="result_by_profile",
                                            title="修正地基承载力特征值计算结果",
                                            fields_meta=fields_metadata, )
        # Create the fields metadata of result_by_foundation
        fields_metadata = [FieldMetadata(name="foundation_name", title="拟建物", unit=Units.UNITLESS),
                           FieldMetadata(name="foundation_width", title="基础底面宽度", unit=Units.m),
                           FieldMetadata(name="bottom_stress", title="设计荷载", unit=Units.kPa),
                           FieldMetadata(name="satisfied", title="是否满足", unit=Units.UNITLESS),
                           ]
        table_result_by_foundation = TableData(data=result_by_foundation, 
                                              name="result_by_foundation",
                                              title="地基承载力是否满足",
                                              fields_meta=fields_metadata, )
        
        # Create the output
        output = TableCollection()
        output.add_table(table_result_by_profile)
        output.add_table(table_result_by_foundation)

        self._ports_out["OutputResult"].data = output
        return output


@status_manage
class SoilUniformity(PipeModule):
    """Evaluate the uniformity of soil below the foundation."""

    def __init__(self, mname: str = "SoilUniformity", auto_run: bool = True,
                 profiles: PortTypeHint.MultiProfile1D | None = None,
                 materials: PortTypeHint.MaterialTable | None = None,
                 foundation: PortTypeHint.RecFoundation | None = None,
                 cal_elevation: float | None = None,
                 fill_Es: float = 2.0) -> None:
        """Initialize SoilUniformity object.
        
        Parameters
        ----------
        profiles: PortTypeHint.MultiProfile1D, default: None
            The profiles used for calculation.

        materials: PortTypeHint.MaterialTable, default: None
            The materials data used for calculation.

        foundation: PortTypeHint.RecFoundation, default: None
            The information of the shallow foundation.

        cal_elevation (m): float, default: None
            The bottom elevation of the deformed zone.
            If not None, the cal_elevation used will be max value of cal_elevation and the max value of elevation of all profiles' bottom.
            If None, it will be the max value of elevation of all profiles' bottom.

        fill_Es (MPa): float, default: 2.0
            The compression modulus of the fill which is used when the bottom of foundation is above the top of profile.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {"InputProfiles": Port(ptype=PortType.MultiProfile1D, data=profiles,
                                                pdoc="The 1D profiles used for calculation."),
                          "InputMaterialTable": Port(ptype=PortType.MaterialTable, data=materials,
                                                 pdoc="The material table used for calculation."),
                          "InputFoundation": Port(ptype=PortType.RecFoundation, data=foundation,
                                                 pdoc="The information of the shallow foundation.")}
        self._ports_out = {"OutputResult": Port(ptype=PortType.SingleResult, pdoc="The calculated result.")}

        self.cal_elevation = cal_elevation
        self.fill_Es = fill_Es

    @property
    def InputProfiles(self) -> None:
        raise AttributeError("Property 'InputProfiles' is write-only.")

    @InputProfiles.setter
    def InputProfiles(self, value: PortTypeHint.MultiProfile1D | None) -> None:
        self["InputProfiles"] = value

    @property
    def InputMaterialTable(self) -> None:
        raise AttributeError("Property 'InputMaterialTable' is write-only.")

    @InputMaterialTable.setter
    def InputMaterialTable(self, value: PortTypeHint.MaterialTable | None) -> None:
        self["InputMaterialTable"] = value

    @property
    def InputFoundation(self) -> None:
        raise AttributeError("Property 'InputFoundation' is write-only.")

    @InputFoundation.setter
    def InputFoundation(self, value: PortTypeHint.RecFoundation | None) -> None:
        self["InputFoundation"] = value

    @property
    def OutputResult(self) -> PortTypeHint.SingleResult | None:
        return self["OutputResult"]

    def set_cal_params(self, reset: bool = True) -> dict[str, RangeModel]:
        values_range = {"cal_elevation": RangeModel(vtype="float", title="变形区底部标高", default=self.cal_elevation, required=False),
                        "fill_Es": RangeModel(vtype="float", title="填土压缩模量", minmax=(0, None), inlcude_min=False,
                                              default=self.fill_Es)}
        return values_range

    def execute(self) -> PortTypeHint.SingleResult | None:
        profiles: PortTypeHint.MultiProfile1D | None = self._ports_in["InputProfiles"].data
        materials: PortTypeHint.MaterialTable | None = self._ports_in["InputMaterialTable"].data
        foundation: PortTypeHint.RecFoundation | None = self._ports_in["InputFoundation"].data

        if profiles is None or materials is None or foundation is None:
            self._ports_out["OutputResult"].data = None
            return None

        # Find the max elevation of profiles
        max_elev = profiles[0].layers_elevation[-1]
        for profile in profiles:
            last_layer_elev = profile.layers_elevation[-1]
            if last_layer_elev > max_elev:
                max_elev = last_layer_elev
        if self.cal_elevation is not None:
            max_elev = max(max_elev, self.cal_elevation)

        # Uniform all the profiles at the same top - foundation bottom and same bottom - max bottom elevation of profile
        cal_profiles: list[PortTypeHint.Profile1D] = []
        for profile in profiles:
            cal_profile = profile.cut_profile(cut_elev=foundation.bottom_elev, keep_below=True, add_fill=True, fill_id=-1)   # cut to foundation bottom
            if cal_profile.top is None:
                raise RuntimeError(f"基础底面标高小于编号为 '{cal_profile.pnum}' 的钻孔底部标高，请检查！")
            cal_profile = cal_profile.cut_profile(cut_elev=max_elev, keep_below=False)  # cut by max elevation
            cal_profiles.append(cal_profile)

        # Add fill compression modulus to material table
        if -1 not in materials.index:    # No fill compression modulus in material table
            materials.add_material({GeoMaterialProps.MaterialID: -1, GeoMaterialProps.MaterialName: "填土", 
                                    GeoMaterialProps.LayerNumber: "0", 
                                    GeoMaterialProps.CompressionModulus: self.fill_Es})

        # Calculate mean compression modulus
        mean_es_cal = RecFoundationSettle(width=foundation.width, length=foundation.length,
                                          ground_elev=foundation.design_ground_elev,
                                          bottom_depth=foundation.bottom_depth)
        mean_Ess = []
        for cal_profile in cal_profiles:
            # get the depth of each interface in the profile
            interfaces_depth = cal_profile.layers_depth.copy()
            interfaces_depth.insert(0, 0)  # Insert the depth of the 1st layer's top
            # Get the compression modules of each layer in the profile
            layers_es = []
            materials_id = cal_profile.materials_id
            for material_id in materials_id:
                layer_es = materials.get_material(material_id)[GeoMaterialProps.CompressionModulus]
                layers_es.append(layer_es)
            mean_Es = mean_es_cal.cal_equivalent_E(profile=interfaces_depth, es=layers_es)
            mean_Ess.append(mean_Es)

        # Calculate the uniformity
        real_K = max(mean_Ess) / min(mean_Ess)
        mean_value_Es = sum(mean_Ess) / len(mean_Ess)
        if mean_value_Es <= 4:
            std_K = 1.3
        elif mean_value_Es <= 7.5:
            std_K = (mean_value_Es - 4) * (1.5 - 1.3) / (7.5 - 4) + 1.3
        elif mean_value_Es <= 15:
            std_K = (mean_value_Es - 7.5) * (1.8 - 1.5) / (15 - 7.5) + 1.5
        elif mean_value_Es <= 20:
            std_K = (mean_value_Es - 15) * (2.5 - 1.8) / (20 - 15) + 1.8
        else:
            std_K = 2.5

        non_uniformity = False
        if real_K > std_K:
            non_uniformity = True

        cal_depth = foundation.bottom_elev - max_elev
        final_result = [UnitResult(name="calculation_depth", title="计算深度", unit=Units.m, value=cal_depth),
                        UnitResult(name="mean_es", title="压缩模量当量平均值Es", unit=Units.MPa, value=mean_value_Es),
                        UnitResult(name="real_non_uniformity_factor", title="实际不均匀系数C_U", unit=Units.UNITLESS, value=real_K),
                        UnitResult(name="standard_non_uniformity_factor", title="标准不均匀系数C_{Uk}", unit=Units.UNITLESS, value=std_K)]
        
        conclusion = UnitResult(name="conclusion", title="评价结果", unit=Units.UNITLESS, value="均匀，满足要求")
        if non_uniformity:
            conclusion.value = "不均匀，不满足要求"
        final_result.append(conclusion)

        output = SingleResult(result=final_result)

        self._ports_out["OutputResult"].data = output
        return output


@status_manage
class BearingCapacityBySPT(PipeModule):
    """Calculate bearing capacity fak of each soil according to standard - JGJ340-2015 - term 7.4.8"""

    def __init__(self, mname: str = "BearingCapacityByCPT", auto_run: bool = True,
                 material_table: PortTypeHint.MaterialTable | None = None, str_precision: int | None = None) -> None:
        """Initialize a BearingCapacityByCPT object.

        Parameters
        ----------
        material_table : PortTypeHint.MaterialTable, default: None
            The material table for calculation.
            Example:
            ```
            materials_table = {MaterialProp.MaterialID: [0, 1, 2],
                              MaterialProp.ModifiedSPTNumber: [6, 8.9, 24],
                              MaterialProp.MaterialName: ["砂土", "黏性土", "粉土"]}
            units = [null, null, null]
            ```

        str_precision : int | None, default: None
            The precision used for string type fak.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {"InputMaterials": Port(ptype=PortType.MaterialTable, data=material_table,
                                                 pdoc="The material table used for calculation.")}
        self._ports_out = {"OutputResults": Port(ptype=PortType.TableData, pdoc="The calculated result.")}

        self.str_precision = str_precision

    @property
    def InputMaterials(self) -> None:
        raise AttributeError("Property 'InputMaterials' is write-only.")

    @InputMaterials.setter
    def InputMaterials(self, value: PortTypeHint.MaterialTable | None) -> None:
        self["InputMaterials"] = value

    @property
    def OutputResults(self) -> PortTypeHint.TableData | None:
        return self["OutputResults"]

    def set_cal_params(self, reset: bool = True) -> dict[str, Any]:
        values_range = {"str_precision": [0, 100]}
        return values_range

    def execute(self) -> PortTypeHint.TableData | None:
        materials: PortTypeHint.MaterialTable | None = self._ports_in["InputMaterials"].data

        if materials is None:
            self._ports_out["OutputResults"].data = None
            return None

        # calculate soil bearing capacity
        results = {GeoMaterialProps.MaterialID: [], GeoMaterialProps.MaterialName: [],
                   GeoMaterialProps.SoilCharacteristicBearingCapacity: [], "fak_str": []}
        for material in materials:
            material_name = material.get(GeoMaterialProps.MaterialName)
            if material_name is None:
                fak = None
            else:
                cal_bc = SoilCapacityByExp(material_name=material_name)
                spt_number = material.get(GeoMaterialProps.ModifiedSPTNumber)
                if spt_number is None:
                    fak = None
                else:
                    fak = cal_bc.cal_foot_capacity_fak(modified_spt_number=spt_number)
            if fak is None:
                fak_str = "无法计算"
            elif isinstance(fak, float):
                if self.str_precision is None:
                    fak_str = str(fak)
                else:
                    fak_str = round(fak, self.str_precision)
                    fak_str = f"{fak_str:.{self.str_precision}f}"
            else:
                fak_str = fak
                fak = None

            results[GeoMaterialProps.MaterialID].append(material[GeoMaterialProps.MaterialID])
            results[GeoMaterialProps.MaterialName].append(material_name)
            results[GeoMaterialProps.SoilCharacteristicBearingCapacity].append(fak)
            results["fak_str"].append(fak_str)

        output = PortTypeHint.TableResult(result=pd.DataFrame(results),
                                          units=[Units.null, Units.null, Units.kPa, Units.kPa],
                                          ch_names=["岩性ID", "岩土名称", "地基承载力特征值", "地基承载力特征值-字符"])

        self._ports_out["OutputResults"].data = output
        return output


@status_manage
class BearingCapacityByStrength(PipeModule):
    """Calculate bearing capacity fa of each soil according to standard - GB50007-2011 - term 5.2.5"""

    def __init__(self, mname: str = "BearingCapacityByStrength", auto_run: bool = True, material_table: PortTypeHint.MaterialTable | None = None,
                 f_width: float = 0.3, anti_float_water_level: float | None = None,
                 str_precision: int | None = None) -> None:
        """Initialize a BearingCapacityByStrength object.

        Parameters
        ----------
        material_table : PortTypeHint.MaterialTable, default: None
            The material table for calculation.
            Example:
            ```
            materials_table = {MaterialProp.MaterialID: [0, 1, 2],
                              MaterialProp.UnitWeight: [17, 18.5, 19],
                              MaterialProp.SaturateUnitWeight: [19, 20, 20],
                              MaterialProp.Cohesion: [10, 15, 18.5],
                              MaterialProp.InternalFrictionAngle: [20, 31, 35],
                              MaterialProp.MeanLayerBottomElevation: [10, 11, 10]}
            units = [null, kN/m3, kN/m3, kPa, °, m, m]
            ```

        f_width : m, float, default: 0.3
            Width of the foundation.

        anti_float_water_level : m, float, default: None
            Water level for the design anti-floating.

        str_precision : int | None, default: None
            The precision used for string type fa.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {"InputMaterials": Port(ptype=PortType.MaterialTable, data=material_table,
                                                 pdoc="The material table used for calculation.")}
        self._ports_out = {"OutputResults": Port(ptype=PortType.TableData, pdoc="The calculated result.")}

        self.f_width = f_width
        self.anti_float_water_level = anti_float_water_level
        self.str_precision = str_precision

    @property
    def InputMaterials(self) -> None:
        raise AttributeError("Property 'InputMaterials' is write-only.")

    @InputMaterials.setter
    def InputMaterials(self, value: PortTypeHint.MaterialTable | None) -> None:
        self["InputMaterials"] = value

    @property
    def OutputResults(self) -> PortTypeHint.TableData | None:
        return self["OutputResults"]

    def set_cal_params(self, reset: bool = True) -> Dict[str, Any]:
        values_range = {"f_width": [0, 1000], "anti_float_water_level": [-1000000, 1000000]}
        return values_range

    def execute(self) -> PortTypeHint.TableData | None:
        materials: PortTypeHint.MaterialTable | None = self._ports_in["InputMaterials"].data

        if materials is None:
            self._ports_out["OutputResults"].data = None
            return None

        # calculate soil bearing capacity
        results = {GeoMaterialProps.MaterialID: [], GeoMaterialProps.SoilCharacteristicBearingCapacity: [], "fa_str": []}
        for material in materials:
            fa = 0
            # get shear strength
            cohesion = material.get(GeoMaterialProps.Cohesion)
            friction = material.get(GeoMaterialProps.InternalFrictionAngle)
            if None in [cohesion, friction]:
                fa = None
            # check water level
            underwater = False
            if self.anti_float_water_level is not None:
                mean_layer_bottom = material.get(GeoMaterialProps.MeanLayerBottomElevation)
                if mean_layer_bottom is None:
                    fa = None
                else:
                    if mean_layer_bottom < self.anti_float_water_level:
                        underwater = True
                    else:
                        underwater = False
            # calculate fa
            if fa is not None:
                cal_fa = SoilCapacityByExp(material_name="soil")
                if underwater:
                    saturate_unit_weight = material.get(GeoMaterialProps.SaturateUnitWeight)
                    if saturate_unit_weight is None:
                        fa = None
                    else:
                        fa = cal_fa.cal_foot_capacity_fa(cohesion=cohesion, internal_friction_angle=friction,
                                                         unit_weight=saturate_unit_weight,
                                                         saturate_unit_weight=saturate_unit_weight,
                                                         underwater=underwater, f_width=self.f_width)
                else:
                    unit_weight = material.get(GeoMaterialProps.UnitWeight)
                    if unit_weight is None:
                        fa = None
                    else:
                        fa = cal_fa.cal_foot_capacity_fa(cohesion=cohesion, internal_friction_angle=friction,
                                                         unit_weight=unit_weight, saturate_unit_weight=unit_weight,
                                                         underwater=underwater, f_width=self.f_width)

            # get string fa
            if fa is None:
                fa_str = "无法计算"
            else:
                if self.str_precision is None:
                    fa_str = str(fa)
                else:
                    fa_str = round(fa, self.str_precision)
                    fa_str = f"{fa_str:.{self.str_precision}f}"

            results[GeoMaterialProps.MaterialID].append(material[GeoMaterialProps.MaterialID])
            results[GeoMaterialProps.SoilCharacteristicBearingCapacity].append(fa)
            results["fa_str"].append(fa_str)

        output = PortTypeHint.TableResult(result=pd.DataFrame(results),
                                          units=[Units.null, Units.kPa, Units.kPa],
                                          ch_names=["岩性ID", "地基承载力特征值", "地基承载力特征值-字符"])

        self._ports_out["OutputResults"].data = output
        return output

